// AspDriver.cs
//
// Implements HomeUX.Components.AspDriver.
//
// See DocFiles\AspDriver.htm for driver usage information.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Xml;
using HomeUX.DriverModel;
using HomeUX.Networking;
using HomeUX.Synchronization;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Implements a HomeUX driver for one or more Sound Control Technologies ASP Audio Matrix 8x8
/// devices connected together to implement an N-input, N-output audio router.
/// </summary>
///
/// <remarks>
/// Concepts: input ID = an input number from 1 to the maximum number of inputs, inclusive;
/// output ID = an output number from 1 to the maximum number of outputs, inclusive;
/// connection ID = 1 or 2, identifies the two connections that can be made from an output to
/// at most two inputs.
/// </remarks>
///
public class AspDriver : Driver
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Constants
    //

    /// <summary>
    /// The pseudo-name of an input which is used for an output that's not connected to any input.
    /// </summary>
    const string NotConnected = "-";

    /// <summary>
	/// The maximum number of inputs that can be connected to a given output.
    /// </summary>
    const int MaxInputsPerOutput = 2;

    /// <summary>
    /// The number of HomeUX-style volume units (where one unit represents the entire range from
	/// silent to maximum loudness) per decibel of gain.
    /// </summary>
    const double VolumeUnitsPerGainDecibel = 0.03;

    /// <summary>
    /// Matches "Input", "Input 2", "Input 3", etc.
    /// </summary>
    static readonly Regex InputRegex = new Regex(@"^Input(?: ([0-9]+))?$");

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

    /// <summary>
    /// The number of ASP units.
    /// </summary>
    int _unitCount;

    /// <summary>
    /// The total number of input channels, also equal to the total number of output channels,
    /// among all ASP units.
    /// </summary>
    int _channelCount;

    /// <summary>
    /// Maps the name of each input specified on the .config file to the corresponding <r>Input</r>
	/// object.  Unused inputs are not included in this dictionary.
    /// </summary>
    Dictionary<string, Input> _mapNameToInput;

    /// <summary>
    /// Maps the 1-based input number of each of the <r>_channelCount</r> inputs to the
	/// corresponding <r>Input</r> object.  Unused inputs are included in this dictionary -- they
	/// are mapped to <n>null</n>.
    /// </summary>
	Dictionary<int, Input> _mapIdToInput;

    /// <summary>
    /// Maps the name of each output specified on the .config file to the corresponding
	/// <r>Output</r> object.  Unused outputs are not included in this dictionary.
    /// </summary>
    Dictionary<string, Output> _mapNameToOutput;

    /// <summary>
    /// Maps the 1-based output number of each of the <r>_channelCount</r> outputs to the
	/// corresponding <r>Output</r> object.  Unused outputs are included in this dictionary -- they
	/// are mapped to <n>null</n>.
    /// </summary>
	Dictionary<int, Output> _mapIdToOutput;

    /// <summary>
    /// The serial port specification string that defines the virtual or real serial port to open.
    /// </summary>
    string _serialPortSpec;

    /// <summary>
    /// Provides device-specific hardware access.
    /// </summary>
    AspHardware _hardware;

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Device Class Overrides & Related Methods
    //

    /// <summary>
    /// Overrides <r>Driver.OnInitialize</r>.
    /// </summary>
    ///
    protected override void OnInitialize()
    {
        // read attributes of the "<Device>" XML element
        _serialPortSpec = Configuration.GetRequiredAttribute("SerialPort");
        _unitCount = Configuration.GetRequiredIntAttribute(
            "Units");

		// each ASP unit has 8 channels; initialize <_channelCount> accordingly
        _channelCount = _unitCount * 8;

        // initialize data structures
        _mapNameToInput = new Dictionary<string, Input>(_channelCount);
		_mapIdToInput = new Dictionary<int, Input>(_channelCount);
        for (int inputId = 1; inputId <= _channelCount; inputId++)
            _mapIdToInput[inputId] = null;
		_mapNameToOutput = new Dictionary<string, Output>(_unitCount);
		_mapIdToOutput = new Dictionary<int, Output>(_unitCount);
        for (int outputId = 1; outputId <= _channelCount; outputId++)
            _mapIdToOutput[outputId] = null;

        // read attributes of child XML element
        foreach (ConfigurationElement element in Configuration.Children)
        {
            switch (element.Name)
            {

            case "Input":

                // set <input> to the "<Input>" element
                var input = new Input()
                {
                    Name = element.GetRequiredAttribute("Name"),
                    InputId = element.GetRequiredIntAttribute("InputId"),
                };

                // ensure that <input.InputId> is a valid input number, and that that input number
                // hasn't already been taken by a previous "<Input>" element
                Input otherInput;
				try
				{
					otherInput = _mapIdToInput[input.InputId];
				}
				catch (KeyNotFoundException)
				{
                    throw new ConfigurationException(element,
						"Invalid input number: {0}",
                        input.InputId);

				}
				if (otherInput != null)
                {
                    throw new ConfigurationException(element,
						"Duplicate input number: {0}",
                        input.InputId);
                }

                // add <input> to <_mapIdToInput>
				_mapIdToInput[input.InputId] = input;

                // ensure that <input.Name> hasn't already been taken by a previous "<Input>"
                // element, and add <input> to <_mapIdToInput>
                try
                {
                    _mapNameToInput.Add(input.Name, input);
                }
                catch (ArgumentException)
                {
                    throw new ConfigurationException(element,
						"Duplicate input name: {0}", input.Name);
                }
                break;

            case "Output":

                // set <output> to the "<Output>" element
                var output = new Output()
                {
                    Name = element.GetRequiredAttribute("Name"),
                    OutputId = element.GetRequiredIntAttribute("OutputId"),
                };

                // ensure that <output.OutputId> is a valid output number, and that that output
				// number hasn't already been taken by a previous "<Output>" element
                Output otherOutput;
				try
				{
					otherOutput = _mapIdToOutput[output.OutputId];
				}
				catch (KeyNotFoundException)
				{
                    throw new ConfigurationException(element,
						"Invalid output number: {0}",
                        output.OutputId);

				}
				if (otherOutput != null)
                {
                    throw new ConfigurationException(element,
						"Duplicate output number: {0}",
                        output.OutputId);
                }

                // add <output> to <_mapIdToOutput>
				_mapIdToOutput[output.OutputId] = output;

                // ensure that <output.Name> hasn't already been taken by a previous "<Output>"
                // element, and add <output> to <_mapIdToOutput>
                try
                {
                    _mapNameToOutput.Add(output.Name, output);
                }
                catch (ArgumentException)
                {
                    throw new ConfigurationException(element,
						"Duplicate output name: {0}",
                        output.Name);
                }
                break;

            }
        }
    }

    /// <summary>
    /// Overrides <r>Driver.OnEnable</r>.
    /// </summary>
    ///
    protected override void OnEnable()
    {
        // enable occurs asynchronously -- queue an AspEnableRequest to perform this operation
        QueueRequest(new AspEnableRequest() { Driver = this }, false);
    }

    /// <summary>
    /// Overrides <r>Driver.OnDisable</r>.
    /// </summary>
    ///
    protected override void OnDisable()
    {
        // disable occurs synchronously -- perform it now
        CloseHardware();
        DisableCompleted();
    }

    /// <summary>
    /// Overrides <r>Driver.SerializeStateForClient</r>.
    /// </summary>
    ///
    /// <param name="sessionId">See <r>Driver.SerializeStateForClient</r>.</param>
    ///
    /// <param name="writer">See <r>Driver.SerializeStateForClient</r>.</param>
    ///
    public override void SerializeStateForClient(Guid sessionId, XmlWriter writer)
    {
        // write the "device-name\Outputs\..." SyncNode updates
        SyncNodeUpdate update;
        foreach (Output output in _mapNameToOutput.Values)
        {
            // set <path> to the SyncPath "device-name\Outputs\output-name"
            SyncPath path = SyncPath.Build(Name, "Outputs", output.Name);

            // write "<path>\Volume" and "<path>\IsMuted"
            SerializeSyncNodeUpdate(writer, output.Name, path, "Volume");
            SerializeSyncNodeUpdate(writer, output.IsMuted, path, "IsMuted");

            // write "<path>\Input" and "<path>\Input 2" (whichever one(s) exist)
            int connectionId = 1;
            foreach (Input input in output.Connections)
            {
				update = CreateConnectionSyncNodeUpdate(output, connectionId, input);
                update.Serialize(writer, false);
                connectionId++;
            }
        }

        // set <inputNamesPath> to "Configuration\device-name\InputNames"
        SyncPath inputNamesPath = SyncPath.Build("Configuration", Name, "InputNames");

        // write "<inputNamesPath>\-"
        SerializeSyncNodeUpdate(writer, "(None)", inputNamesPath, NotConnected);

        // write each "<inputNamesPath>\input-name"
        foreach (Input input in _mapNameToInput.Values)
            SerializeSyncNodeUpdate(writer, input.Name, inputNamesPath, input.Name);

        // set <outputNamesPath> to "Configuration\device-name\OutputNames"
        SyncPath outputNamesPath = SyncPath.Build("Configuration", Name, "OutputNames");

        // write each "<outputNamesPath>\output-name"
        foreach (Output output in _mapNameToOutput.Values)
            SerializeSyncNodeUpdate(writer, output.Name, outputNamesPath, output.Name);
    }

    /// <summary>
    /// Returns a <r>SyncNodeUpdate</r> that represents a given connection of a given <r>Output</r>
	/// being connected to a given <r>Input</r>.
    /// </summary>
	///
    /// <param name="output">The <r>Output</r>.</param>
	///
    /// <param name="connectionId">The connection ID, between 1 and <r>MaxInputsPerOutput</r>,
	/// 	inclusive.</param>
	///
    /// <param name="input">The <r>Input</r> that's connected to <pr>output</pr>, or <n>null</n> if
	/// 	connection <pr>connectionId</pr> of <pr>output</pr> is disconnected.</param>
    ///
	SyncNodeUpdate CreateConnectionSyncNodeUpdate(Output output, int connectionId, Input input)
	{
		return new SyncNodeUpdate()
		{
			Path = SyncPath.Build(Name, "Outputs", output.Name,
				((connectionId == 1) ? "Input" : String.Format("Input {0}", connectionId))),
			Value = NullIfDisabled((input != null) ? input.Name : NotConnected)
		};
	}

    /// <summary>
    /// See <r>Driver.ProcessSyncNodeUpdate</r>.
    /// </summary>
    ///
    /// <param name="originator">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="update">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="replyBuilder">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    public override void ProcessSyncNodeUpdate(Guid originator, SyncNodeUpdate update,
        MessageBuilder replyBuilder)
    {
        // a valid SyncPath to update is "device-name\top-level-component\..."; switch based on
        // "top-level-component"
        switch (update.Path.MatchRequiredPart(1, "Outputs"))
        {

        case 0: // "Outputs"

            ProcessSyncNodeUpdate_Outputs(originator, update, replyBuilder);
            break;

        }
    }

    /// <summary>
    /// Called when a client requests an update to a SyncTree node that's of the form
    /// "device-name\Outputs\...".
    /// </summary>
    ///
    /// <param name="originator">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="update">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="replyBuilder">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    void ProcessSyncNodeUpdate_Outputs(Guid originator, SyncNodeUpdate update,
        MessageBuilder replyBuilder)
    {
        // <update.Path> should be: "device-name\Outputs\output-name\control-name"
        // Example: "ASP\Outputs\Living Room NW\Volume"

		// set <outputName> to the "output-name" SyncPath component (see above); error if it's not
		// found or if it doesn't refer to an existing output; set <output> to the Output object
        string outputName = update.Path.GetRequiredPart(2);
        Output output;
        if (!_mapNameToOutput.TryGetValue(outputName, out output))
            throw new SyncException(update, "Invalid output name \"{0}\"", outputName);

		// set <controlName> to the "control-name" SyncPath component (see above); error if it's
		// not found
        string controlName = update.Path.GetRequiredLastPart(3);

		// interpret <controlName>
        int connectionId;
        if (controlName == "Volume")
        {
            // <update.Path> is "device-name\Outputs\output-name\Volume"
			QueueRequest(new AspOutputVolumeChangeRequest()
			{
				Driver = this,
                Operation = update,
				Originators = new List<Guid>() { originator },
				Output = output,
				NewVolume = LimitVolume(update.GetTypedValue<double>())
			}, true);
        }
        else
        if (controlName == "IsMuted")
        {
            // <update.Path> is "device-name\Outputs\output-name\IsMuted"
            QueueRequest(new AspOutputIsMutedChangeRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                Output = output,
                NewIsMuted = update.GetTypedValue<bool>()
            }, true);
        }
        else
        if ((connectionId = ParseConnectionSpec(controlName, update)) > 0)
		{
            // <update.Path> is "device-name\Outputs\output-name\Input" or
            // <update.Path> is "device-name\Outputs\output-name\Input 2" etc.
			QueueRequest(new AspConnectionRequest()
			{
				Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
				Output = output,
				ConnectionId = connectionId,
				NewInput = ParseInputName(update.GetTypedValue<string>(), update)
			}, true);
		}
        else
        {
            // unknown <controlName>
            throw new SyncException(update,
                "\"{0}\" is not valid -- expecting one of: Volume, IsMuted, {1}",
                controlName, String.Join(", ", GetConnectionSpecs()));
        }
    }

    /// <summary>
    /// Overrides <r>Driver.MergeRequests</r>.
    /// </summary>
    ///
    /// <param name="futureRequests">See <r>Driver.MergeRequests</r>.</param>
    ///
    protected override void MergeRequests(IList<Driver.Request> futureRequests)
    {
        // strategy: we only look at the initial set of AspOutputVolumeChangeRequest requests
        // (i.e. stopping at the first one that isn't such a request), and we look for cases
        // where request B is later (within this set) than request A, where A and B change
        // volume for the same output, and in that case we delete A since B overrides it
        while (true)
        {
            // we need at least 2 requests in order to do a merge
            if (futureRequests.Count < 2)
                return;

            // set <first> to the first request; return if it's not an AspOutputVolumeChangeRequest
			// request; note that we'll repeat this (since we're in a loop)
            var first = futureRequests[0] as AspOutputVolumeChangeRequest;
            if (first == null)
                return;

            // <first> is request A (in the comments above); set <other> to request B; if we find
            // B, remove A, but also add the originator (client session ID) of A to B (effectively
            // merging the requests); if we don't find B, stop looking for requests to merge (if
            // there are other requests that could be merged presumably they were already merged
            // during previous calls to ProcessSyncNodeUpdate)
            AspOutputVolumeChangeRequest other = FindRequestToMergeInto(futureRequests, first);
            if (other != null)
            {
                other.Originators.AddRange(first.Originators);
                futureRequests.RemoveAt(0);
            }
            else
                break;
        }
    }

    /// <summary>
    /// Assuming the first request in the list of future <r>Driver.Request</r> objects is a
    /// given <r>AspOutputVolumeChangeRequest</r>, this method scans whatever other
    /// <r>AspOutputVolumeChangeRequest</r> requests follow it (i.e. stopping at the first request
    /// that isn't <r>AspOutputVolumeChangeRequest</r>) to find the first such request that refers
    /// to the same output, and then returns that request (since it will override the first
    /// request).  If no such request is found, <n>null</n> is returned.
    /// </summary>
    ///
    /// <param name="futureRequests">The list of future <r>Driver.Request</r> objects, i.e.
    ///     requests that waiting to be processed.</param>
    ///
    /// <param name="first">The first request in <pr>futureRequests</pr>, which is an
    ///     <r>AspOutputVolumeChangeRequest</r></param>
    ///
    AspOutputVolumeChangeRequest FindRequestToMergeInto(IList<Driver.Request> futureRequests,
        AspOutputVolumeChangeRequest first)
    {
        for (int requestIndex = 1; requestIndex < futureRequests.Count; requestIndex++)
        {
            var other = futureRequests[requestIndex] as AspOutputVolumeChangeRequest;
            if (other == null)
                return null;
            if (other.Output == first.Output)
                return other;
        }

        return null;
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    /// <summary>
    /// Implements <n>IDisposable.Dispose</n>.
    /// </summary>
    ///
    public override void Dispose()
    {
        if (_hardware != null)
        {
            _hardware.Dispose();
            _hardware = null;
        }
        base.Dispose();
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Other Methods
	//

    /// <summary>
    /// Assuming <r>_hardware</r> is null, this method creates an <r>AspHardware</r> object, used
    /// to access ASP hardware, and sets <r>_hardware</r> to refer to it.
    /// </summary>
	///
    void OpenHardware()
    {
        VirtualSerialPort serialPort = null;
        try
        {
            // set <serialPort> to a new serial port, as specified by <_serialPortSpec>
            serialPort = VirtualSerialPort.GetPort(_serialPortSpec);

            // set <_hardware> to a newly-allocated AspHardware object that will use <serialPort>
            // to communicate with the hardware
            _hardware = new AspHardware(WorkQueue, serialPort, Name, _unitCount);

            // hook up <_hardware> events
            // IMPORTANT: do corresponding "-=" in CloseHardware
            _hardware.CommandFailed += OnHardwareCommandFailed;
            _hardware.CommandTimedOut += OnHardwareCommandTimedOut;
            _hardware.NotificationsReceived += OnHardwareNotificationReceived;
            _hardware.WarningsReported += OnHardwareWarningsReported;
            // IMPORTANT: do corresponding "-=" in CloseHardware

            // open <_hardware>; this will fail if the serial port cannot be opened
            _hardware.Open();
        }
        finally
        {
            // if <serialPort> was allocated but ownership of it wasn't transferred to <_hardware>,
			// free <serialPort> to prevent a memory leak
            if ((serialPort != null) && (_hardware == null))
            {
                serialPort.Dispose();
                serialPort = null;
            }
        }

    }

    /// <summary>
    /// Assuming <r>_hardware</r> is not null, this method frees it.
    /// </summary>
    ///
    void CloseHardware()
    {
        // disconnect <_hardware> events
        _hardware.CommandFailed -= OnHardwareCommandFailed;
        _hardware.CommandTimedOut -= OnHardwareCommandTimedOut;
        _hardware.NotificationsReceived -= OnHardwareNotificationReceived;
        _hardware.WarningsReported -= OnHardwareWarningsReported;

		// dispose of <_hardware>
        _hardware.Dispose();
        _hardware = null;
    }

    /// <summary>
    /// See <r>AspHardware.CommandFailed</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>AspHardware.CommandFailed</r>.</param>
    ///
    /// <param name="e">See <r>AspHardware.CommandFailed</r>.</param>
    ///
    public void OnHardwareCommandFailed(object sender, AspHardwareCommandFailedEventArgs e)
    {
        // report the failure to clients
        ReportAlert(MessageAlertType.Warning, "\"{0}\" command failed", e.Command);
    }

    /// <summary>
    /// See <r>SimpleSerialHardware{}.CommandTimedOut</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>SimpleSerialHardware{}.CommandTimedOut</r>.</param>
    ///
    /// <param name="e">See <r>SimpleSerialHardware{}.CommandTimedOut</r>.</param>
    ///
    public void OnHardwareCommandTimedOut(object sender,
        SimpleSerialHardwareCommandTimedOutEventArgs e)
    {
        // report the timeout to clients
        ReportAlert(MessageAlertType.Warning, "\"{0}\" command timed out", e.Command);
    }

    /// <summary>
    /// See <r>AspHardware.NotificationsReceived</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>AspHardware.NotificationsReceived</r>.</param>
    ///
    /// <param name="e">See <r>AspHardware.NotificationsReceived</r>.</param>
    ///
    public void OnHardwareNotificationReceived(object sender,
		AspHardwareNotificationsReceivedEventArgs e)
    {
        // report the notifications to clients
        BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
        {
            foreach (string notification in e.Notifications)
            {
                messageBuilder.WriteAlertElement(MessageAlertType.Warning,
                    MessagePrefix + "Notification: {0}", notification);
            }
        });
    }

    /// <summary>
    /// See <r>AspHardware.WarningsReported</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>AspHardware.WarningsReported</r>.</param>
    ///
    /// <param name="e">See <r>AspHardware.WarningsReported</r>.</param>
    ///
    public void OnHardwareWarningsReported(object sender, AspHardwareWarningsReportedEventArgs e)
    {
        foreach (string message in e.Messages)
            ReportAlert(MessageAlertType.Warning, message);
    }

    /// <summary>
    /// Returns an array of the valid connection specifications, i.e. "Input", "Input 2",
	/// etc.
    /// </summary>
    ///
    string[] GetConnectionSpecs()
    {
        string[] result = new string[MaxInputsPerOutput];
        for (int index = 0; index < result.Length; index++)
        {
            if (index == 0)
                result[index] = "Input";
            else
                result[index] = String.Format("Input {0}", index + 1);
        }

        return result;
    }

    /// <summary>
    /// Given a volume level (a <n>double</n>), this method returns the level limited to the
    /// range 0.0 to 1.0, inclusive.
    /// </summary>
    ///
    /// <param name="volume">The given volume level.</param>
    ///
    double LimitVolume(double volume)
    {
        if (volume < 0)
            return 0;
        else
        if (volume > 1)
            return 1;
        else
            return volume;
    }

    /// <summary>
    /// Converts an ASP decibel gain value to a volume and mute setting.
    /// </summary>
    ///
    /// <param name="gain">A decibel gain value; 0 means "full volume" positive values decrease the
    ///     volume.  -1 means "mute".</param>
    ///
    /// <param name="volume">A volume level from 0.0 to 1.0, inclusive.</param>
    ///
    /// <param name="mute">A mute setting.</param>
    ///
    static void GainToVolumeAndMute(decimal gain, out double volume, out bool mute)
    {
        if (gain < 0)
        {
            // note that the ASP hardware doesn't remember the volume of a muted output; since
            // this output is muted, we'll just set an arbitrary (but fairly quiet) volume
            volume = 0.2;
            mute = true;
        }
        else
        {
            volume = GainToVolume(gain);
            mute = false;
        }
    }

    /// <summary>
    /// Converts a decibel gain level (zero or above) to a volume level (0.0 to 1.0, inclusive),
	/// which is returned.
    /// </summary>
    ///
    /// <param name="gain">A gain value; 0 means "full volume" positive values decrease the
    ///     volume.  -1 is not valid for this method.</param>
    ///
    static double GainToVolume(decimal gain)
    {
        return Math.Max(0,
			Math.Min(1, Math.Round(1 - VolumeUnitsPerGainDecibel * (double)gain, 3)));
    }

    /// <summary>
    /// Converts a volume level (0.0 to 1.0, inclusive) to a decibel gain value (0 or above), which
	/// is returned.
    /// </summary>
    ///
    /// <param name="volume">A volume level from 0.0 to 1.0, inclusive.</param>
    ///
    static decimal VolumeToGain(double volume)
    {
        return (decimal) Math.Max(0, Math.Min(31, (1 - volume) / VolumeUnitsPerGainDecibel));
    }

    /// <summary>
    /// Parses an input name, and returns the corresponding <r>Input</r> object, or <n>null</n> if
	/// the input name is "-" (which means "no input").
    /// </summary>
    ///
    /// <param name="inputName">The input name.</param>
    ///
    /// <param name="update">The <r>SyncNodeUpdate</r> that contained
    ///     <pr>connectionSpec</pr>.  Used for error reporting purposes.</param>
    ///
    /// <exception cref="SyncException">
    /// <pr>inputName</pr> is invalid.
    /// </exception>
    ///
	Input ParseInputName(string inputName, SyncNodeUpdate update)
	{
		Input input;
		if (inputName == NotConnected)
			return null;
		else
		if (_mapNameToInput.TryGetValue(inputName, out input))
			return input;
		else
			throw new SyncException(update, "Input \"{0}\" not found", inputName);
	}

    /// <summary>
    /// Parses a connection spec of the form "Input" (which returns 0), "Input 2" (which return 1),
	/// "Input 3" (which returns 2), etc.  Returns the parsed connection ID in the range 1 to
	/// <r>MaxInputsPerOutput</r>, inclusive -- this value is one more than an index into
	/// <r>Output.Connections</r>.   If the given string isn't of the form "Input [<i>number</i>]",
	/// 0 is returned; if it is, but <i>number</i> is out of range, a <r>SyncException</r> is
	/// thrown.
    /// </summary>
    ///
    /// <param name="connectionSpec">The connection specification.</param>
    ///
    /// <param name="update">The <r>SyncNodeUpdate</r> that contained
    ///     <pr>connectionSpec</pr>.  Used for error reporting purposes.</param>
    ///
    /// <exception cref="SyncException">
    /// <pr>connectionSpec</pr> is invalid.
    /// </exception>
    ///
	int ParseConnectionSpec(string connectionSpec, SyncNodeUpdate update)
	{
        // parse <connectionSpec>; return 0 if it's not of the form "Input [<number>]"
		Match match = InputRegex.Match(connectionSpec);
		if (!match.Success)
			return 0;

        // parse the number, if provided
        string numberAsString = match.Groups[1].Value;
        if (numberAsString.Length > 0)
        {
            try
            {
                int connectionId = XmlConvert.ToInt32(match.Groups[1].Value);
                if ((connectionId <= 0) || (connectionId > MaxInputsPerOutput))
                    throw new FormatException();
                return connectionId;
            }
            catch (Exception ex)
            {
                if ((ex is FormatException) || (ex is OverflowException))
                {
                    throw new SyncException(update, "Invalid connection specification: {0}",
                        connectionSpec);
                }
                else
                    throw;
            }
        }
        else
            return 1; // "Input" is the same as "Input 1"
	}

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // AspRequest and Derived Types
    //

    /// <summary>
    /// A request from a client.
    /// </summary>
    ///
    abstract class AspRequest : Driver.Request
    {
        /// <summary>
        /// The driver that owns this request.
        /// </summary>
        protected new AspDriver Driver
        {
            [DebuggerStepThrough]
            get
            {
                return (AspDriver)base.Driver;
            }
        }
    }

    /// <summary>
    /// A requst to enable the device.
    /// </summary>
    ///
    [DebuggerDisplay("Enable driver")]
    class AspEnableRequest : AspRequest, Driver.IEnableRequest
    {
        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            // open the hardware
            try
            {
                Driver.OpenHardware();
            }
            catch (VirtualSerialPortException ex)
            {
                Error = Util.FormatExceptionMessage(ex);
                yield break; // end AspRequest
            }

            // query the hardware for its full status: the volume level of outputs, which inputs
			// are connected to which outputs, etc.
            var getFullStatus = new ResultHolder<AspGetFullStatusReply>(this);
            Driver._hardware.GetFullStatus(getFullStatus.OnDone);
            yield return true; // wait for async reply
            if (getFullStatus.Result == null)
            {
                Error = "GetFullStatus failed";
                yield break; // end AspRequest
            }

            // For each output declared in the .config file, we do the following:
            //   -- Get the output level and use it to initialize Output.Volume and Output.IsMuted.
            //   -- Find the which inputs are connected to that output with unity gain (0 dB), and
            //      copy that information to Output.Connections.
            //        -- If any input is connected to the output but not at unity gain, we'll be
            //           disconnecting that input.
            //        -- If more than MaxInputsPerOutput of the remaining inputs for the output
            //           are connected, we'll be disconnecting them.
            //        -- If any input is not specified in the .config file as being in use, but
            //           <reply> reported that input as being connected to this output, we'll be
            //           disconnecting the input.
            //
            // For each output that's not declared in the .config file as being in use, we'll
            // ignore that output.
            //
            var crosspointsToDisconnect = new List<Crosspoint>();
            foreach (Output output in Driver._mapNameToOutput.Values)
            {
                // initialize <output.Volume> and <output.IsMuted> based on the output's gain level
                // as reported in <reply>
                GainToVolumeAndMute(getFullStatus.Result.OutputGains[output.OutputId - 1],
					out output.Volume, out output.IsMuted);

                // enumerate inputs to see which inputs are connected to <output>, and make
                // <output.Connections> match the state of the hardware
                int previousConnectionId = 0;
                for (int inputId = 1; inputId <= Driver._channelCount; inputId++)
                {
                    Input input = Driver._mapIdToInput[inputId];
                    if (input != null)
                    {
                        decimal gain =
							getFullStatus.Result.CrosspointGains[inputId - 1][output.OutputId - 1];
                        if (gain < 0)
                        {
                            // <input> is not connected to <output>
                        }
                        else
                        if (previousConnectionId >= MaxInputsPerOutput)
                        {
                            // <input> is connected to <output>, but we've already found as many
                            // inputs connected to this output as this driver can handle --
                            // additional inputs need to be disconnected
                            crosspointsToDisconnect.Add(new Crosspoint(inputId, output.OutputId));
                        }
                        else
                        {
                            // record the fact that <input> is connected to <output>
                            output.Connections[previousConnectionId++] = input;
                        }
                    }
                    else
                    {
                        // input <inputId> is not specified in the config file as being in use;
                        // if <reply> reports it as being connected to this output, we need to
                        // disconnect it
                        decimal gain =
							getFullStatus.Result.CrosspointGains[inputId - 1][output.OutputId - 1];
                        if (gain >= 0)
                            crosspointsToDisconnect.Add(new Crosspoint(inputId, output.OutputId));
                    }
                }
            }

            // disconnect crosspoints as specified by <crosspointsToDisconnect>
            foreach (Crosspoint crosspoint in crosspointsToDisconnect)
            {
				var setCrosspointGain = new ResultHolder<AspSetCrosspointGainReply>(this);
				Driver.Trace(true, "HW: Disconnect crosspoint {0}->{1} (due to initialization)",
                    crosspoint.InputId, crosspoint.OutputId);
                Driver._hardware.SetCrosspointGain(crosspoint.InputId, crosspoint.OutputId, -1,
                    setCrosspointGain.OnDone);
                yield return true; // wait for async reply
                if (setCrosspointGain.Result == null)
                {
                    Error = "SetCrosspointGain failed";
                    yield break; // end AspRequest
                }
            }
        }
    }

    /// <summary>
    /// A request to change the volume of an output.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
	class AspOutputVolumeChangeRequest : AspRequest
	{
        /// <summary>
        /// The output that will be affected.
        /// </summary>
		public Output Output;

        /// <summary>
        /// The new value for <r>Output.Volume</r>.
        /// </summary>
		public double NewVolume;

        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            // set the hardware level to <NewVolume>; if the output is muted, there's nothing to
			// tell the hardware at this point -- we'll use <NewVolume> when the output is unmuted
            if (!Output.IsMuted)
            {
                var setOutputGain = new ResultHolder<AspSetOutputGainReply>(this);
                decimal gain = VolumeToGain(NewVolume);
				Driver.Trace(true, "HW: Output {0} ({1}): Set Volume={2:n2} ({3})",
                    Output.OutputId, Output.Name, gain, NewVolume);
                Driver._hardware.SetOutputGain(Output.OutputId, gain,
                    setOutputGain.OnDone);
                yield return true; // wait for async reply
                if (setOutputGain.Result == null)
                {
                    Error = "SetOutputGain failed";
                    yield break;
                }
            }

            // update driver state
            Output.Volume = NewVolume;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Change {0} Volume to {1}",
                    Output.DebuggerDisplay, NewVolume);
            }
        }
    }

    /// <summary>
    /// A request to mute or un-mute an output.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
    class AspOutputIsMutedChangeRequest : AspRequest
    {
        /// <summary>
        /// The output that will be affected.
        /// </summary>
        public Output Output;

        /// <summary>
        /// The new value for <r>Output.IsMuted</r>.
        /// </summary>
        public bool NewIsMuted;

        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            var setOutputGain = new ResultHolder<AspSetOutputGainReply>(this);
            Driver.Trace(true, "HW: Output {0} ({1}): Set IsMuted={2}",
                Output.OutputId, Output.Name, NewIsMuted);
            Driver._hardware.SetOutputGain(Output.OutputId,
                (NewIsMuted ? -1 : VolumeToGain(Output.Volume)),
                setOutputGain.OnDone);
            yield return true; // wait for async reply
            if (setOutputGain.Result == null)
            {
				Error = "SetOutputGain failed";
                yield break;
            }

            // update driver state
            Output.IsMuted = NewIsMuted;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Change {0} IsMuted to {1}",
                    Output.DebuggerDisplay, NewIsMuted);
            }
        }
    }

    /// <summary>
    /// A request to change which input is connected to a given output.
    /// </summary>
    [DebuggerDisplay("{DebuggerDisplay}")]
    class AspConnectionRequest : AspRequest
	{
        /// <summary>
        /// The output that will be affected.
        /// </summary>
        public Output Output;

        /// <summary>
        /// Which of the output's connections will be affected.  This is a number from 1 to
		/// <r>MaxInputsPerOutput</r>, inclusive.
        /// </summary>
        public int ConnectionId;

        /// <summary>
        /// The input to connect to connection <r>ConnectionId</r> of <r>Output</r>, or <n>null</n>
		/// to if nothing should be connected to that input.
        /// </summary>
        public Input NewInput; // null to disconnect

        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            // if the output connection is currently connected to something else, disconnect it
            Input oldInput = Output.Connections[ConnectionId - 1];
            if (oldInput != null)
            {
                var setCrosspointGain = new ResultHolder<AspSetCrosspointGainReply>(this);
				Driver.Trace(true, "HW: Disconnect {0}->{1} ({2} -> {3})",
                    oldInput.InputId, Output.OutputId, oldInput.Name, Output.Name);
                Driver._hardware.SetCrosspointGain(oldInput.InputId, Output.OutputId, -1,
                    setCrosspointGain.OnDone);
                yield return true; // wait for async reply
                if (setCrosspointGain.Result == null)
                {
					Error = "SetCrosspointGain #1 failed";
                    yield break;
                }
                else
                    Output.Connections[ConnectionId - 1] = null;
            }

            // if the application wants to connect the output connection, do so
            if (NewInput != null)
            {
                var setCrosspointGain = new ResultHolder<AspSetCrosspointGainReply>(this);
                Driver.Trace(true, "HW: Connect {0}->{1} ({2} -> {3})",
                    NewInput.InputId, Output.OutputId, NewInput.Name, Output.Name);
                Driver._hardware.SetCrosspointGain(NewInput.InputId, Output.OutputId, 0,
                    setCrosspointGain.OnDone);
                yield return true; // wait for async reply
                if (setCrosspointGain.Result == null)
                {
					Error = "SetCrosspointGain #2 failed";
                    yield break;
                }
            }

            // update driver state
            Output.Connections[ConnectionId - 1] = NewInput;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
		///
        public object DebuggerDisplay
        {
            get
            {
				if (NewInput != null)
				{
					return new EncapsulatedString("Connect {0} connection #{1} to {2}",
						Output.DebuggerDisplay, ConnectionId, NewInput.DebuggerDisplay);
				}
				else
				{
					return new EncapsulatedString("Disconnect {0} connection #{1}",
						Output.DebuggerDisplay, ConnectionId);
				}
            }
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Other Nested Types
    //

    /// <summary>
    /// Identifies a possible connection point between an input and an output.
    /// </summary>
	///
    [DebuggerDisplay("InputID={InputId}, OutputId={OutputId}")]
    class Crosspoint
    {
        /// <summary>
        /// A 1-based input ID.
        /// </summary>
        public int InputId;

        /// <summary>
        /// A 1-based output ID.
        /// </summary>
        public int OutputId;

        /// <summary>
        /// Initializes an instance of this class.
        /// </summary>
        /// 
        /// <param name="inputId">The value for <r>InputId</r>.</param>
        /// 
        /// <param name="outputId">The value for <r>OutputId</r>.</param>
        /// 
        public Crosspoint(int inputId, int outputId)
        {
            InputId = inputId;
            OutputId = outputId;
        }
    }

    /// <summary>
    /// Represents a single input channel on the Audio Matrix unit.  Two inputs can be grouped
	/// into a stereo input, but that concept is above the level of this driver.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
    class Input
    {
        /// <summary>
        /// The name of this input.
        /// </summary>
        public string Name;

        /// <summary>
        /// The 1-based hardware input number of this input.
        /// </summary>
        public int InputId;

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Input \"{0}\" (#{1})", Name, InputId);
            }
        }
    }

    /// <summary>
    /// Represents a single output channel on the Audio Matrix unit.  Two outputs can be grouped
	/// into a stereo output, but that concept is above the level of this driver.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
    class Output
    {
        /// <summary>
        /// The name of this output.
        /// </summary>
        public string Name;

        /// <summary>
        /// The 1-based hardware output number of this output.
        /// </summary>
        public int OutputId;

        /// <summary>
        /// The inputs currently connected to this output.  <n>null</n> means no connection.  If
        /// multiple inputs are connected to one output, the audio from those inputs is mixed.
        /// </summary>
		public Input[] Connections = new Input[MaxInputsPerOutput];

        /// <summary>
        /// The output volume level, in the range 0.0 to 1.0 (inclusive).  If <r>IsMuted</r> is
        /// <n>true</n> then the output is silent regardless of the value of <r>Volume</r>.
        /// </summary>
        public double Volume;

        /// <summary>
        /// <n>true</n> if this output is muted, <n>false</n> if not.
        /// </summary>
        public bool IsMuted;

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Output \"{0}\" (#{1})", Name, OutputId);
            }
        }
    }
}

}

